上一篇粗略地提到了原始型別與複合型別。
這篇我們想來討論一下各原始型別的特色,讓我們後面使用型別能打好更好的基礎。
複合型別我們後面單獨抽一篇出來討論關於物件。
null
與 undefined
null 指的是空值,undefined 指的是未定義,兩者可能容易在一開始被混淆,儘管他們看起來相似,但仍有許多不同的地方。
let a;
console.log(a);//undefined
function foo() {}
console.log(foo()); // undefined
let obj = {};
console.log(obj.prop); // undefined
在以上三種情況:
而 null 則是表示空值,無值的情況的。
let y = null;
console.log(y); // null
let obj = { a: 123 };
obj = null;
與未定義不同,null 多為有意地表示空值,第一個例子表明 y 初始時是一個空值,而第二個例子表明現在 obj 從物件改為空值,不再指向一個物件。
在 JavaScript 中,有時 null 會被有意地用於表示空值或重置行為。
而不管針對 null 或是 undefined,若對這兩種對象進行屬性的存取,皆會拋出錯誤。寫程式時應充分考慮實際例子是否會遇到這兩種型別,做出對應處理。
let a;
try{console.log(a)}catch(error){console.log('catch',,error.name, error.message)}
//"catch", "TypeError", "Cannot read properties of undefined (reading 'a')"
let b = null;
try{console.log(b.b)}catch(error){console.log('catch',error.name, error.message)}
//"catch", "TypeError", "Cannot read properties of null (reading 'b')"
boolean
單純的型別,只有 true / false 兩個值,用於表示真假
number
與 bigint
兩種在 JavaScript 中表達數字的型別,number 使用 64-bit 浮點數格式,可以表達整數與小數,表達範圍從 2^53 到 2^53,操作數字超出上下限時會失去精度。
bigint 在 ES2020 被引入,僅限於表示整數,但無範圍上限,可表任意精度的整數,適合處理非常大的整數。bigint 的宣告是在數字最後加上 n 來表示這個數字是一個 bigint。
let num = 9007199254740991;
console.log(num + 1); // 9007199254740992
console.log(num + 2); // 9007199254740992 (失去精度)
let bigIntValue = 9007199254740991n;
console.log(bigIntValue + 1n); // 9007199254740992n
console.log(bigIntValue + 2n); // 9007199254740993n
bigint 和 nubmer 在使用上不可混用,若要計算,必須顯式轉換,否則會遇到 TypeError 的例外報錯。
let num = 10;
let bigIntValue = 10n;
try{console.log(num + bigIntValue);}catch(error){console.log('catch',error.name, error.message)}
//"catch", "TypeError", "Cannot mix BigInt and other types, use explicit conversions"
關於 number,JavaScript 是使用二進制來儲存,這樣會導致的結果就是除了超出上下限的數字以外,有些數字也會失去部分精度。如 0.1 在十進制能表達為一個簡單的有限小數,但若要把 0.1 在二進制中表現,則會變成:
0.1 (10進制) = 0.00011001100110011001100110011001100110011001100110011... (2進制)
這個二進制小數是無限循環,有點類似 1/3 在十進位中的感覺,所以 JS 在儲存這個值的時候,會儲存為一個近似的二進制數字,並把近似值留在尾端。當然也有小數是能夠不失去精度的表示,如同 0.5,是 2 的 -1 次方,2^(-1),二進位計數為 0.1。
我們來看看若是無法完整表示的數字會發生什麼事:
let a = 0.1;
console.log(a + a);//0.2
console.log(a + a + a);//0.30000000000000004
如同上面的例子,當兩個近似值相加,就會像 0.3 尾數的 4 的部分。一般來說,如果是沒有那麼要求精度的情況,為了顯示與儲存,通常會使用 toFixed
這個 Number 的內建函式。
let a = 0.1;
let b = a + a + a;
let c = b.toFixed(2);
console.log(a, b, c);//0.1, 0.30000000000000004, "0.30"
console.log(typeof c);// "string"
這個函式做的事情是,四捨五入至傳入參數的小數後位數,如上方例子,便是捨去到小數點後第二位。另外可以注意到,當指定為 2 時,剩餘位數會補 0 至指定位數,如 0.30 的尾數 0,且該函式輸出是一個字串,並非數字(便於顯示以及能夠表顯尾數 0 的形式)。
如果是需要高度精度的場合,則可以選擇使用其他 JS 函式庫,如 decimal.js 等等,部分函式庫會使用將數字轉為字串,使用逐位相加的方式來做整數運算來避免精度丟失。
string
字串,用於儲存與表現文字的型別。
宣告字串有三種方式 '', "", ``
let a = 'str';
let b = "str";
let c = `str :
${b}`;//str : str
console.log(a,b,c);
//"str", "str", "str :
//str"
單引號與雙引號用法上完全相同,但在雙引號中的單引號會被視為一般的字元,反之亦然。
let d = '"str"';
let e = "'str'";
console.log(d,e);
//"\"str\"", "'str'"
通常使用哪種並沒有一定,端看各個團隊的編碼風格,盡量保持一貫性即可。
而 `` (反引號,back-tick)是 ES6 後新加入的功能,稱作 樣板字面量 Template Literals(Template strings),用於更優雅的在字串裡接入變數。
一如上面 c
的例子,使用錢字號搭配大括弧${},中間放入變數名稱,該變數便會被隱式轉換為字串顯示。
另外,使用``的字串,能夠合法的跨行,不用像以前一樣需要寫"\n"+...這樣的方式來做多行字串的表現,是個大大增進程式字串可讀性和靈活性的語法。
另外記得,因為 string 是原始型別,儘管 string 提供了非常多基於 String 型別的函式,由於原始型別的不可變性,所有函式的結果都不會影響本來的變數,若需留存結果,應以新的變數儲存。
let a = 'abc';
console.log(a.toUpperCase());//ABC
console.log(a);//abc
symbol
ES 6 引入的原始型別,日常開發遇到這個型別的機會相較其他型別來的低。
通常用於物件的屬性名稱命名,避免屬性名稱的衝突,且所有 symbol 皆具唯一性,無論 symbol 和另外的哪個 symbol 相比皆不相等,也無法與任何其他的型別進行比較,不會被隱式轉換。
宣告 symbol 型別時使用 Symbol()進行宣告。
let sym1 = Symbol('foo');
let sym2 = Symbol('foo');
console.log(sym1 === sym2); // false
console.log(sym1 == sym2); // false
因為 symbol 的唯一性,能夠避免同名稱的屬性被意外地覆蓋,以下為幾個不使用 symbol 會遇到的問題與如何使用 symbol 解決
let obj = {};
obj.name = 'object';
//假設在另一個地方,沒有意識到 name 已經被使用了,想要初始化 name
obj.name = 'object2';
//則原本的 'object' 就被意外的覆寫
//使用 symbol
let sym = Symbol('name');
obj[sym] = 'object';
//在另一個地方,假設我們要初始化 name 這個屬性,我們使用 symbol 來做
let sym2 = Symbol('name');
obj[sym2] = 'object2';
console.log(obj[sym],obj[sym2]);
//object, obejct2,並沒有發生覆寫的情況
let sym = Symbol('foo');
let obj = {
[sym]: 'bar',
a:'b'
};
console.log(Object.keys(obj));
//["a"]
console.log(JSON.stringfy(obj));
//"{\"a\":\"b\"}"
上面例子只印出了 a 這個鍵值,即使透過 JSON.stringfy 也不會印出 bar 的值,這是因為 symbol 本身的特性,能用於創建不想被發現的屬性。
但仍有方法能夠找到物件上的 symbol,請見下面的例子
let sym = Symbol('foo');
let obj = {
[sym]: 'bar',
a:'b'
};
console.log(String(Object.getOwnPropertySymbols(obj)[0]));//"Symbol(foo)"
console.log(Reflect.ownKeys(obj));//["a", [object Symbol] { ... }]
另外這個例子可以看出,Symbol 本身若進行隱式轉換變成字串時,會顯示為 [object Symbol],若要顯示 symbol 的內容,我們可以使用 String(symbol) 的方式來印出可讀結果。
本篇介紹了各個原始型別該如何宣告,以及各原始型別的特性。這篇文章中出現了幾次「顯式轉換」和「隱式轉換」,== 和 ===,我們下篇會進一步來討論這些細節。